I’ve recently been programming seriously in C, after around 10 years in higher level languages (Go, Python, C++, and others). I’ve been using C11, the latest standard, whereas previously I was working in C89.
I like programming in C. It’s not an easy language to write fluently because it doens’t provide many conveniences, it’s full of traps, and I’d avoid it if I was writing something that needed to be safe, but I still find it fun.
This post describes my recent experience with C and the new standard, and my observations about how things have and haven’t changed. It’s not a critique of C, and some of the obvious problems with writing C (such as lack of bounds checking of arrays) aren’t discussed.
The good
First the good things about C11 (relative to C89 — these things may have appeared in in-between standards).
The new standard integer types, int64_t
,
uint32_t
, int8_t
and so on, are a very welcome addition. Previously,
typically one would have to generate a header file with platform-specific
definitions with the same effect, but it’s nice to have it in the standard library.
That you need to write, for example, printf("a 64 bit number: %" PRIu64 "\n", (uint64_t)42);
to print one out is pretty ugly though.
Similarly, it’s nice to have a bool
type built into the language.
Being able to specify struct field names, and array indices in literals and initializers helps readability (and, like in Go, allows one to extend structs with backwards-compatibility if code treats zero values carefully).
I find myself writing code like this quite often, which initializes a struct pointer’s fields, including zeroing the unspecified ones:
*item = (layerItem_s){
.props = LAYER_ITEM_PROP_ELEMENT_TRIANGLES | LAYER_ITEM_PROP_BLEND | LAYER_ITEM_PROP_TEX0,
.vao_index = vao,
.offset = buf_offset / sizeof(font_vertex_s),
.n_elements = vx_count,
.tex0 = texture,
};
Many functions in the standard library that set errno
now have a “safe” _s
variant that
returns an errno_t
rather than using a global (or thread-local) variable to report the errors.
For example:
FILE *f;
errno_t err = fopen_s(&f, "data.txt", "r");
... code that checks err.
The lack of multiple return-values makes this a bit ugly, but this seems like a step forward to
me, both because it makes error-checking a bit more obvious, but also because it avoids the
problems with the global errno
.
Having clang-format
is really nice, for all the usual reasons that automated code-formatters are nice.
Finally, compilation speed with clang is tolerable. On my development machine, I can run a parallel make, and get a binary in around 2 seconds. Ideally it would be faster, but it’s quite acceptable.
Things I miss
Having to write separate .h
files, and in general, the lack of modules or packages or whatever
else you might call them, is a chore. The Go model of packages possibly being multiple files,
and having a way of specifying what’s public and what’s not works really well, and the bonus
of getting explicit dependency information makes the build process more easily toolable. I don’t
want to have to write a Makefile
to incrementally compile a library or binary. I don’t claim
that adding modules/packages to C is straightforward, but it’s something I found a bit painful.
I missed something like C++’s RAII or go’s defer
. With explicit memory management, in many
cases one wants to allocate at the start of a block and free at the end, and not have
unruly code to handle the frees when you break out of the block or return from a function mid-block.
I also missed objects. Nearly all mainstream languages now have a way of specifying methods
for objects. Being able to define methods (even if there’s no generics, interfaces or inheritance),
is a natural way of grouping functionality with the associated data, and the method names
are not in any global or package namespace, so they can be shorter. I’d much rather read a function call
that takes an item and an integer that looks like item.F(123)
than how you’d write it using
the convention of prefixing function names with the package that defines them: item_F(item, 123)
.
I’d really like a smarter printf
, which I guess would require much smarter macros like in zig.
Not only would it mean that for example you could write %d
whatever the specific integer type, but also
allow better error-checking, and perhaps even enable custom formatting code.
The awful
Concurrency and parallelism are still not properly in the language. I was extremely
happy to see a threads module in the standard library (threads.h
), but the I discovered it
was an optional extension, and isn’t available everywhere (including on the
clang distribution I’m using). The obvious alternative choice on Linux is pthreads, but on Windows
either you use the Windows API, or download a port of pthreads from sourceforge.net. What a mess.
Debug information is not standardized. Linux has a nice execinfo.h
module that provides a
simple way to get backtraces, and Windows has a quite complicated API that also requires
separate debug symbol files. I just want debug information compiled into my debug build, and
for there to be a standard API for parsing the stack.
Dependencies are awful. In my code, I’ve minimized my third-party dependencies, and only currently depend on OpenGL and SDL2. But still, I found it extremely painful to find the right libraries to link to, and OpenGL in particular has become user-hostile, requiring “glew” on Windows to use the latest versions. I still haven’t managed to build a statically-linked binary on Windows using the clang compiler and SDL.
Conclusion
Overall, I found the newer features of C11 I used beneficial, but relatively superficial.
I consider the lack of availability of threads.h
to be the biggest disappointment,
given the modern world with its multi-core CPUs.
Mostly, I found C to be as good as it ever was. Fun and efficient, but error-prone and with rough usability.